package com.quectel.util.kit;


import com.google.common.base.Preconditions;
import com.quectel.util.ex.redis.CacheToRedisException;
import com.quectel.util.redis.RedisUtils;
import com.quectel.util.thread.ThreadPoolUtils;
import lombok.extern.slf4j.Slf4j;

import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.function.Supplier;

/**
 * 缓存帮助类
 *
 * @author caolu
 * @email louis.cao@quectel.com
 */
@Slf4j
public class CacheKit {

    /**
     * redis空值标识
     */
    private static final String NULL_FLAG = "REDIS_CACHE_NULL_VALUE";
    /**
     * 重新缓存锁
     */
    private static final String RE_CACHE_LOCK_KEY = "RE_CACHE_LOCK_KEY";
    /**
     * 延长key的有效期
     */
    private static final String EXTEND_TIME_LOCK = "EXTEND_TIME_LOCK";

    /**
     * 最少缓存时间
     */
    private static final int AT_LEAST_EXTEND_CACHE_MINUTES = 1;
    /**
     * 最长缓存时间
     */
    private static final int AT_MOST_EXTEND_CACHE_MINUTES = 5;


    /**
     * 缓存续期
     */
    private static final ThreadPoolExecutor POOL = ThreadPoolUtils.createCommonIOBoundPool("cache-to-redis-pool-");

    private CacheKit() {

    }


    /**
     * 删除缓存
     *
     * @param cacheKeys 对象缓存到redis的key
     */
    public static void invalidRedisCache(String... cacheKeys) {

        for (String cacheKey : cacheKeys) {
            RedisUtils.del(cacheKey);
        }
    }

    /**
     * 主要用户DB记录缓存到redis
     *
     * @param supplier    产生一个对象 要求可序列化 例如 DB查询<br>
     *                    <pre>lambda需要强转:(Supplier&lt;SysConfigEntity&gt;) ()->db.getSomeValue()</pre>
     * @param cacheKey    对象缓存到redis的key
     * @param liveSeconds 缓存时间 实际缓存时间会多1-5分钟防止key同时失效
     * @param <T>
     * @return 优先返回redis中的值 redis没有缓存则计算supplier的返回值进行缓存
     */
    public static <T> T cacheToRedis(Supplier<T> supplier, String cacheKey, int liveSeconds) {

        if (!RedisUtils.isReady()) {
            //没有初始化
            return supplier.get();
        }

        Preconditions.checkState(liveSeconds > 0, "Cache to Redis time must greater than 0");

        Object object = RedisUtils.getBinaryObject(cacheKey);
        if (object != null) {
            Long ttl = RedisUtils.ttl(cacheKey, TimeUnit.SECONDS);
            //小于我们最小的缓存时间 我们进行续期
            if (ttl > 0 && ttl < AT_LEAST_EXTEND_CACHE_MINUTES * 60) {
                //开始续期 续期前要加锁 可以不释放 续期不成功可以走cache=null的重新加锁的缓存方法
                if (RedisUtils.setnx(EXTEND_TIME_LOCK + cacheKey, "", AT_LEAST_EXTEND_CACHE_MINUTES, TimeUnit.MINUTES)) {
                    //只能有一个线程续期 我们锁定60 一般可以完成重新缓存
                    POOL.execute(() -> {
                        //多缓存1~5分钟
                        int randomLiveSeconds = (new SecureRandom().nextInt(AT_LEAST_EXTEND_CACHE_MINUTES) + AT_MOST_EXTEND_CACHE_MINUTES) * 60;
                        T t = supplier.get();
                        if (t == null) {
                            RedisUtils.setBinaryObject(cacheKey, NULL_FLAG, liveSeconds + randomLiveSeconds, TimeUnit.SECONDS);
                        } else {
                            //不为空 null值缓存的时间稍微短一点
                            RedisUtils.setBinaryObject(cacheKey, t, randomLiveSeconds, TimeUnit.SECONDS);
                        }
                    });
                }
            }
            if (Objects.equals(object, NULL_FLAG)) {
                return null;
            }
            return (T) object;
        }
        //redis缓存为null 开始真正执行业务逻辑进行缓存 同时只能有一个线程缓存
        Lock spinLock = RedisUtils.getSpinLock(RE_CACHE_LOCK_KEY + cacheKey);
        spinLock.lock();
        try {
            //检查是否已经缓存(上一个线程是否缓存完成) //double check
            Object reGetObject = RedisUtils.getBinaryObject(cacheKey);
            if (reGetObject != null) {
                if (Objects.equals(reGetObject, NULL_FLAG)) {
                    return null;
                }
                return (T) reGetObject;
            }
            //多缓存1~5分钟
            int randomLiveSeconds = (new SecureRandom().nextInt(AT_LEAST_EXTEND_CACHE_MINUTES) + AT_MOST_EXTEND_CACHE_MINUTES) * 60;
            T t = supplier.get();
            if (t == null) {
                RedisUtils.setBinaryObject(cacheKey, NULL_FLAG, liveSeconds + randomLiveSeconds, TimeUnit.SECONDS);
            } else {
                //不为空 null值缓存的时间稍微短一点
                RedisUtils.setBinaryObject(cacheKey, t, randomLiveSeconds, TimeUnit.SECONDS);
            }
            return t;
        } catch (Exception e) {
            throw new CacheToRedisException(e);
        } finally {
            spinLock.unlock();
        }
    }


}
